package vivace.view;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.Observable;
import java.util.Observer;
import java.util.Vector;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import vivace.model.*;

import vivace.helper.NoteHelper;
import vivace.model.Action;
import vivace.model.Bar;
import vivace.model.MusicalObject;
import vivace.model.Note;
import vivace.model.Project;

public class NoteSystem extends JPanel implements Observer {

    /**
     * 
     */
    private static final long serialVersionUID = -5077559669220640299L; //LOLWTFBBQ?
    private Project model;
    private Controller controller;

    //private long resolution; // In PPQ

    private Font font;
    private Font altfont;

    private Note selectedNote;

    private static final String SYSTEM = "\uD834\uDD1A";
    private static final String TREBLE_CLEF = "\uD834\uDD1E";
    private static final String BASS_CLEF = "\uD834\uDD22";

    public static final String BAR = "\uD834\uDD00";
    public static final String FINAL_BAR = "\uD834\uDD02";

    //private Vector<MidiEvent> shortMessageEvents;
    private Vector<Bar> bars;
    private int fontSize;

    /**
     * 
     * @param model
     */
    public NoteSystem(Project model) {
        super(null);
        this.model = model;    
        controller = new Controller();
        //this.setLayout(null); // use absolute positioning

        //AppSystem..addStaticObserver(this);

        App.addProjectObserver(this,App.Source.ALL);

        //resolution = model.getPPQResolution();
        bars = new Vector<Bar>();

        //font = new Font("Euterpe", Font.PLAIN, 60);
        // XXX: For testing purposes, I set the font size randomly, so that I can see if all symbols add up at all sizes.
        //fontSize =  20+new Random().nextInt(50);
        fontSize = 40;
        font = new Font("Euterpe", Font.PLAIN, (fontSize * App.UI.getZoomLevel())/100);
        altfont = new Font("Arial", Font.PLAIN, fontSize);

        initiateNoteSystem();

    }

    public void initiateNoteSystem() {

        // Remove all old notes.
        removeAll();

        //        System.out.println("NoteSystem.initateNoteSystem");
        // Create a list of all musical objects, and put them into bars.
        // TODO: Lite fulhack f�r att komma �t f�rsta markerade sp�ret (som vi dessutom inte har n�gon ordning p�)
        // Kanske l�gga till st�d f�r flera markerade sp�r �ven h�r?
        int selected=0;
        if (App.UI.getTrackSelection().size() == 0){
            selected = 0;
            // System.out.println("No tracks selected?!?!");
        } else{
            for( Integer i : App.UI.getTrackSelection()){
                selected = i;
                //System.out.printf("Selected track: %d\n",i);
                //     break;
            }

            //        System.out.println("Vi har markerade!");

            if(selected !=0){
                bars = NoteHelper.createBars(NoteHelper.createMusicalObjectList(model.getTrack(0),model.getTrack(selected), model.getPPQResolution()), model.getPPQResolution());
                setPreferredSize(new Dimension(180000, getHeight())); // TODO: remove hardcoded width
            }
            //else
            //{ System.out.printf("No tracks selected!\n");}
        }
    }






    public void paintComponent(Graphics g) {

        super.paintComponent(g);

        font = new Font("Euterpe", Font.PLAIN, (fontSize * App.UI.getZoomLevel())/100);
        // fill background area with white color
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, getWidth(), getHeight());
        g.setColor(Color.BLACK);

        g.setPaintMode();

        g.setFont(font);

        this.setOpaque(false);

        int noteSystemInc = g.getFontMetrics().stringWidth(SYSTEM);
        int barLength = g.getFontMetrics().stringWidth(BAR);

        /* print out clefs  */
        int trebleClefPosition = g.getFontMetrics().getHeight(); // XXX: Varför kan jag inte sätta en variabel till g.getFontMetrics().getHeight() istället? Varför blir det fel då?
        int bassClefPosition = Math.round(g.getFontMetrics().getHeight() * 1.8f); 

        g.drawString(TREBLE_CLEF, 25, trebleClefPosition);
        g.drawString(BASS_CLEF, 25, bassClefPosition);

        int i = 25+ g.getFontMetrics().stringWidth(TREBLE_CLEF), x = 25;

        // This is the difference in height between different notes when drawn
        //float noteDrawnDifference = g.getFontMetrics().getHeight()*(1/18f - 1/200f - 1/1000f);

        int j=0;
        NoteLabel tempnl=new NoteLabel(new Note(0,0,0),0,0);
        for (Bar b : bars) { 

            g.drawString(BAR, i, trebleClefPosition);
            g.drawString(BAR, i, bassClefPosition);
            i+=barLength;
            Note n;
            NoteLabel nl;
            for ( MusicalObject m : b.getObjects() ){ 
                // draw note system lines 
                g.drawString(SYSTEM, x, trebleClefPosition);
                g.drawString(SYSTEM, x, bassClefPosition);

                x = x + noteSystemInc;

                // Check whether this musical object is drawable. Which is always should be, imho.
                if(m.toString() != null){


                    if(m instanceof Note){
                        j++; // Just to keep track of the number of notes we have.
                        n = (Note) m;
                        // And now, we draw the note at the afforementioned position, rounded off to the nearest integer.
                        //nl = new NoteLabel(n, i, noteDrawnPosition,noteDrawnDifference);
                        nl = new NoteLabel(n,i,trebleClefPosition);
                        nl.addMouseListener(controller);
                        this.add(nl);

                        NoteHelper.print("m",n);

                        if(n.getNoteTiedFrom() !=null){
                            drawArc(tempnl,nl, g);
                        }

                        if(n.getNoteTiedTo() != null){
                            tempnl = nl;
                        }

                        if( n.getMIDINumber() < 41){
                            drawLine(nl,g);
                        }

                        if(n.isMiddleC()){
                            drawLine(nl,g);
                        }

                        if(n.getMIDINumber() > 80 ){
                            System.out.printf("%d ",n.getRelOffset());
                            if(n.getRelOffset()%2 == 0)
                                drawLine(nl,g);
                            //else
                            //    drawLine(new Note(n.getMIDINumber()-1,0,n.getPPQResolution), g);
                        }


                        i+=g.getFontMetrics().stringWidth(n.toString());


                    } else {
                        NoteHelper.print("o",m);
                        g.drawString(m.toString(), i, trebleClefPosition);
                        i+=g.getFontMetrics().stringWidth(m.toString());
                    }
                } else {
                    if (m instanceof TimeSignature){
                        // TODO: Perhaps the best would be to replace this with two or three different labels/lines that together made up a TimeSignature
                        DenominatorLabel tsnl = new DenominatorLabel((TimeSignature) m, i, trebleClefPosition);
                        NumeratorLabel tsdl = new NumeratorLabel((TimeSignature) m, i, trebleClefPosition);
                        DenominatorLabel tsnbl = new DenominatorLabel((TimeSignature) m, i, bassClefPosition);
                        NumeratorLabel tsdbl = new NumeratorLabel((TimeSignature) m, i, bassClefPosition);
                        this.add(tsnl);
                        this.add(tsdl);
                        this.add(tsnbl);
                        this.add(tsdbl);
                        i+=tsnl.width();
                    }
                }
            }
        }

        /* If there weren't a lot of notes (perhaps none) to draw,
         * at least draw an empty note system that spans the entire
         * panel's width
         */

        while (x < getWidth()) {

            /* draw note system lines */
            g.drawString(SYSTEM, x, trebleClefPosition);
            g.drawString(SYSTEM, x, bassClefPosition);
            x = x + noteSystemInc;
        }


    }

    /**
     * Paints a tie over a note.
     */
    public void drawLine(NoteLabel nl, Graphics g){

        Note n = nl.getNote();
        FontMetrics fm = nl.getFontMetrics();

        int startx = (int)(nl.getX() +  noteAccidentalDisplacement(n,fm));
        int endx = (int)(startx + fm.stringWidth(Note.WHOLE)* 0.9f);
        //int starty = nl.getY() + (int) (nl.getNoteDrawnDifference() * 12.5);
        int starty = nl.getY() + (int)(12.5*nl.getNoteDrawnDifference());
        for(int k = App.UI.getZoomLevel()/50 - 1, i = -k, j=2*k; i<=k;i++,j+=(i>0)?1:-1)
        {
            g.drawLine(startx+j, starty+i,endx-j, starty+i);
        }
    }

    /**
     * Either the note should have raised pitch (ex. C should maybe be C#)
     */
    public int noteAccidentalDisplacement(Note n, FontMetrics fm){
    return n.displaysAccidental() ? n.isSharp() ? fm.stringWidth(Note.SHARP): fm.stringWidth(Note.FLAT):0;
    }
    
    /**
     * Paints a tie between the two noteLabels on the Graphics g
     */
    public void drawArc(NoteLabel startNote, NoteLabel endNote, Graphics g){
        FontMetrics fm = startNote.getFontMetrics();
        Note sn = startNote.getNote();
        Note en = endNote.getNote();
        int startx = startNote.getX() + (sn.displaysAccidental() ? sn.isSharp() ? fm.stringWidth(Note.SHARP): fm.stringWidth(Note.FLAT):0);
        int endx = endNote.getX() + (en.displaysAccidental() ? en.isSharp() ? fm.stringWidth(Note.SHARP): fm.stringWidth(Note.FLAT):0);
        int curvature = (int)(fm.getHeight()/5f);

        int ypos = endNote.getY()+(int)(14*endNote.getNoteDrawnDifference());
        drawArc(startx,endx,ypos,curvature, g);

    }

    /**
     * Paints a tie between the note at position startX and the one at endX, taking
     * into account the current font size, and using the note position y.
     * In case you don't want to set the curvature of the tie yourself, the default value
     * is 8.
     */
    public  void drawArc(int startX, int endX, int y, Graphics g){
        drawArc(startX,endX,y,16,g);
    }

    /**
     * Paints a tie between the note at position startX and the one at endX, taking
     * into account the current font size, and using the note position y.
     * In case you don't want to set the curvature of the tie yourself, the default value
     * is 8.
     */
    public  void drawArc(int startX, int endX, int y, int curvature, Graphics g){
        int arclength = endX - startX;

        int arcStartingAngle = 200;
        int arcExtent = 150;

        // sätt noteOffset till längden på en halv helnot, dvs börja rita bågen en halv not in.
        int noteOffset = g.getFontMetrics().stringWidth("\uD834\uDD5D")/2;

        // ritar ett antal bågar som tillsammans blir en snygg båge som är tjockare i mitten än i kanterna.
        // +=2 är för att det inte skall bli avrundningsfel i hörnen, utan att alla skall börja och sluta i samma punkt.
        for(int i = 0;i<5;i+=2)
        {
            //if(NoteHelper.PRINT_BEAM_DEBUG)
            //    System.out.printf("drawArc(%d,%d,%d,%d,%d,%d)", startX+noteOffset,y-(curvature+i)/2,arclength,curvature+i,arcStartingAngle,arcExtent);
            g.drawArc(startX+noteOffset,y-(curvature+i)/2,arclength,curvature+i,arcStartingAngle,arcExtent);
        }

    }

    @Override
    public void update(Observable o, Object arg) {
        this.repaint();

        // First of all, check if we really need to bother about the update 
        if( App.UI.getPerspective() != UI.Perspective.NOTATION){
            return;
        }

        Action a = (Action) arg;

        switch (a) {
            case ZOOMLEVEL_CHANGED: // Ändra fontstorlek! \o/
                repaint();
                initiateNoteSystem(); // This is needed to avoid the big ugly grapihcs bug that makes notes not erase themselves.
                break;
            //case RESOLUTION_CHANGED: // Ändra noggrannheten när vi approximerar?
            case PERSPECTIVE_CHANGED:
            case TRACK_SELECTION_CHANGED:
            case RECORDING_FINISHED:
                initiateNoteSystem();
                break;

        }

    }

    private class Controller implements MouseListener, KeyListener {

        public void mouseClicked(MouseEvent e) {
            // TODO Auto-generated method stub

        }

        public void mouseEntered(MouseEvent e) {
            // TODO Auto-generated method stub

        }

        public void mouseExited(MouseEvent e) {
            // TODO Auto-generated method stub

        }

        public void mousePressed(MouseEvent e) {
            // TODO Auto-generated method stub
            NoteLabel l = (NoteLabel) e.getSource();
            //System.out.println("note clicked!");
            selectedNote = l.getNote();

            l.setForeground(Color.RED);

        }

        public void mouseReleased(MouseEvent e) {
            // TODO Auto-generated method stub

        }

        public void keyPressed(KeyEvent e) {
            if (selectedNote != null) {
                Bar b = selectedNote.getBar();

                switch(e.getKeyCode()) {
                    case KeyEvent.VK_DELETE:
                        b.remove(selectedNote);
                }

                repaint();

            }
        }

        public void keyReleased(KeyEvent arg0) {
            // TODO Auto-generated method stub

        }

        public void keyTyped(KeyEvent e) {
            // TODO Auto-generated method stub

        }

    }

    private class DenominatorLabel extends JLabel {
        private FontMetrics fm;
        private TimeSignature ts;
        public DenominatorLabel(TimeSignature ts, int x, int trebleClefPosition){
            //super( (new Integer(ts.getNumerator()).toString())+(new Integer(ts.getDenominator()).toString()));
            //super( new Integer(ts.getNumerator()).toString());
            super();
            String s =  (new Integer(ts.getDenominator()).toString());

            setText(s);
            setForeground(Color.BLACK);
            Font arial = new Font("Arial", Font.PLAIN, (int)(fontSize/3f * App.UI.getZoomLevel())/100);
            setFont(arial);
            fm=this.getFontMetrics(arial);
            this.ts = ts;
            int yPos = trebleClefPosition - fm.getHeight();
            //this.setBounds(x, trebleClefPosition - fm.getHeight(), fm.stringWidth(new Integer(ts.getNumerator()).toString()), fm.getHeight()); 
            this.setBounds(x, yPos, fm.stringWidth(s),  fm.getHeight()); 
        }
        public int width(){
            return fm.stringWidth(new Integer(ts.getNumerator()).toString());
        }
    }

    private class NumeratorLabel extends JLabel {
        private FontMetrics fm;
        private TimeSignature ts;
        public NumeratorLabel(TimeSignature ts, int x, int trebleClefPosition){
            //super( (new Integer(ts.getNumerator()).toString())+(new Integer(ts.getDenominator()).toString()));
            //super( new Integer(ts.getNumerator()).toString());
            super();
            String s =  (new Integer(ts.getNumerator()).toString());

            setText(s);
            setForeground(Color.BLACK);
            Font arial = new Font("Arial", Font.PLAIN, (int)(fontSize/3f * App.UI.getZoomLevel())/100);
            setFont(arial);
            fm=this.getFontMetrics(arial);
            this.ts = ts;
            int yPos = trebleClefPosition - fm.getHeight()- fm.getHeight();
            //this.setBounds(x, trebleClefPosition - fm.getHeight(), fm.stringWidth(new Integer(ts.getNumerator()).toString()), fm.getHeight()); 
            this.setBounds(x, yPos, fm.stringWidth(s),  fm.getHeight()); 
        }
        public int width(){
            return fm.stringWidth(new Integer(ts.getNumerator()).toString());
        }
    }


    private class NoteLabel extends JLabel {
        /**
         * 
         */
        private static final long serialVersionUID = 6654762285808284520L;

        private Note note;

        private int relNoteMIDIPosition;
        private float relNoteDrawnPosition;
        private int noteDrawnPosition;
        private int x;
        private int trebleClefPosition;
        private float magicalArrangementConstant;
        private FontMetrics fm;
        private float noteDrawnDifference;

        /**
         * takes in a note, a y-position in the note system, and where the g clef is
         */
        public NoteLabel(Note note, int x, int trebleClefPosition){
            super(note.toString()); 
            setHorizontalTextPosition(SwingConstants.LEADING);
            setForeground(Color.BLACK);
            setFont(font);
            this.note = note;
            this.x = x;
            this.trebleClefPosition = trebleClefPosition;

            fm = this.getFontMetrics(font);

            noteDrawnDifference = this.getFontMetrics().getHeight()*(1/18f - 1/200f - 1/1000f);
            // This is the note offset counted from the middle C on the piano.
            relNoteMIDIPosition =  note.getRelOffset();
            // This is where we will draw the note, relative the standard note.
            relNoteDrawnPosition = relNoteMIDIPosition * noteDrawnDifference ;
            // This is the note position when we draw it.
            // The three added noteDrawnDifferences are because I originally used F instead of C as reference note.
            // If anyone wants to recalibrate everything to make the formulae more aestetically pleasing, well, by all means go ahead... ;)
            magicalArrangementConstant = 1+1/3f-1/100f;
            if(relNoteMIDIPosition >0 ) {
                // Draw the note relative the treble clef.
                noteDrawnPosition = Math.round(magicalArrangementConstant*trebleClefPosition+3*noteDrawnDifference-relNoteDrawnPosition);
            } else if (relNoteMIDIPosition == 0) {
                // Middle C
                noteDrawnPosition = Math.round(magicalArrangementConstant*trebleClefPosition+5*noteDrawnDifference);
            } else {
                // Draw the note relative the bass clef.
                noteDrawnPosition = Math.round(magicalArrangementConstant*trebleClefPosition+7*noteDrawnDifference-relNoteDrawnPosition);
            }

            noteDrawnPosition -= fm.getHeight();


            this.setBounds(x,noteDrawnPosition, fm.stringWidth(note.toString()), fm.getHeight()); 
        }


        public float getNoteDrawnDifference(){
            return noteDrawnDifference;
        }

        public int getX(){
            return x;
        }

        public int getY(){
            return noteDrawnPosition;
        }

        public Note getNote() {
            return note;    
        }

        public int getTrebleClefPosition(){
            return trebleClefPosition;
        }

        public FontMetrics getFontMetrics(){
            return fm;
        }
    }
}
